// hhh 封装的fs，这里的所有方法都不使用回调方式，均会拦截throw的error，不会抛出错误的，放心直接使用
import fs from 'fs'
import util from 'util'
import path from 'path'
import stream from 'stream'

const contentTypeMap = {
  'image/tiff': 'tif',
  'image/gif': 'gif',
  'image/x-icon': 'ico',
  'image/jpeg': 'jpg',
  'image/png': 'png',
  'image/webp': 'webp',
  'image/vnd.wap.wbmp': 'wbmp',
}

/**
 * 使用文件做锁
 * ps. 为了防止其它客户端误解锁，解锁需要传入这个方法返回的key
 * @param lockname
 * @param expire 锁持续时间，单位毫秒
 * */
export async function fileLock(lockname: string, expire: number) {
  const pa = path.join(__dirname, `lock_file_${lockname}`)
  const res = await saveFile(pa, (expire + Date.now()).toString())
  if (!res) {
    const file = await readFile(pa)
    if (!file) {
      return false
    }
  }
}

/**
 * 可以传 contentType 的字符串，也可以传文件名，返回的扩展名是没有 . 号的
 * */
export function getFileExt(file: string) {
  return (
    contentTypeMap[file as keyof typeof contentTypeMap] ||
    (file.match(/\.([^.?#]+)(\?.*)?(#.*)?$/) || [])[1] ||
    ''
  )
}

// 获取一个路径中，文件夹的路径，最后一段如果不是以 / 结尾，则认为不是文件夹
export function getPath(dir: string) {
  // /[^/]+\/?$/i UNIX
  // /[^\\]+\\?$/i WINDOW
  return dir.replace(new RegExp(`[^\\${path.sep}\\/]+$`), '')
}

/**
 * 创建多级目录，如果成功，或者目录早已存在，则返回true，否则返回false
 * @param dir string 需要创建的文件夹
 * @return boolean
 * */
export async function mkdir(dir: string) {
  if (!dir) {
    return false
  }
  try {
    await util.promisify(fs.mkdir)(dir, {
      recursive: true,
    })
    return true
  } catch (e) {
    let msg = '文件夹创建失败：'
    if (e.code === 'EEXIST') {
      return true
    } else if (e.code === 'ENOENT') {
      // 它的上级目录不存在，只有 recursive 不为true才会有这种情况
      msg = '上级目录不存在：'
    }
    console.log(msg, e.code, e.message)
  }
  return false
}

// 给文件/文件夹重命名，成功返回true，否则返回false
export async function rename(oldPath: string, newPath: string) {
  try {
    await util.promisify(fs.rename)(oldPath, newPath)
    return true
  } catch (e) {
    return false
  }
}

/**
 * 保存文件
 *
 * ps. 这个方法默认会覆盖文件全部内容
 *
 * @param filePath string 文件路径，要含文件名和扩展名，反正这个方法里不会自动给你加
 * @param fileData string | buffer 可以是字符串，也可以是buffer，是要保存的内容
 * @return boolean 保存成功还是失败
 * */
export function saveFile(filePath: string, fileData: string | Buffer) {
  return mkdir(filePath.split(path.sep).slice(0, -1).join(path.sep)).then(
    function (succ: boolean) {
      if (!succ) {
        return false
      }

      return new Promise((resolve, reject) => {
        if (!(fileData instanceof Buffer)) {
          fileData = Buffer.from(fileData)
        }

        // 块方式写入文件
        const wstream = fs.createWriteStream(filePath)

        wstream.on('open', () => {
          const blockSize = 128
          const nbBlocks = Math.ceil(fileData.length / blockSize)
          for (let i = 0; i < nbBlocks; i++) {
            const currentBlock = fileData.slice(
              blockSize * i,
              Math.min(blockSize * (i + 1), fileData.length)
            )
            wstream.write(currentBlock)
          }

          wstream.end()
        })
        wstream.on('error', (err: any) => {
          console.log('文件存储失败：', err)
          resolve(false)
        })
        wstream.on('finish', () => {
          resolve(true)
        })
      })
    }
  )
}

/**
 * 和上面的差不多，但是可以选择是追加到文件最后
 *
 * ps. 上面那个适合一次性写很多数据
 *
 * @param file
 * @param data
 * @param flag https://nodejs.org/dist/latest-v10.x/docs/api/fs.html#fs_file_system_flags
 * @return 成功会返回true，失败返回false
 * */
export async function writeFile(
  file: string,
  data: any,
  {flag}: {flag?: string} = {}
) {
  try {
    await util.promisify(fs.writeFile)(file, data, {flag})
    return true
  } catch (e) {
    console.error('hFs.writeFile', e)
    return false
  }
}

/**
 * 保存上传上来的文件
 * @param filePath string 文件存储路径，要含文件名和扩展名，反正这个方法里不会自动给你加
 * @param tempPath string 文件缓存在哪
 * @return boolean 保存成功还是失败
 * */
export async function saveUploadFile(filePath: string, tempPath: string) {
  try {
    if (!(await mkdir(filePath.split(path.sep).slice(0, -1).join(path.sep)))) {
      return false
    }

    return await new Promise((resolve, reject) => {
      // 创建可读流
      const reader = fs.createReadStream(tempPath)
      // 创建可写流
      const wstream = fs.createWriteStream(filePath)
      // 可读流通过管道写入可写流
      reader.pipe(wstream)

      wstream.on('finish', () => {
        resolve(true)
      })
      wstream.on('error', err => {
        reject(err)
      })
    })
  } catch (e) {
    console.error('[saveUploadFile]文件保存失败', e.stack)
    return false
  }
}

/**
 * 读取文件
 * */
export async function readFile(filePath: string) {
  try {
    return await util.promisify(fs.readFile)(filePath)
  } catch (e) {
    return false
  }
}

// 读取目录
export async function readdir(path: string, options?: any) {
  try {
    return await util.promisify(fs.readdir)(path, options)
  } catch (e) {
    return false
  }
}

// 删除文件
export async function unlink(path: string) {
  try {
    return await util.promisify(fs.unlink)(path)
  } catch (e) {
    return false
  }
}

// 删除目录
export async function rmdir(path: string) {
  try {
    return await util.promisify(fs.rmdir)(path)
  } catch (e) {
    return false
  }
}

/**
 * 获取路径/文件的状态，返回的对象可以有很多判断方法
 * https://nodejs.org/dist/latest-v10.x/docs/api/fs.html#fs_fs_stat_path_options_callback
 * @return fs.Stats
 * */
export async function stat(dir: string) {
  try {
    return await util.promisify(fs.stat)(dir)
  } catch (e) {
    return false
  }
}

/**
 * 创建一个可读 stream ，循环调用 getData 函数获取数据，当 该函数 返回 null 时结束，如果返回undefined，会认为是返回空字符串
 * @param getData size参数是用于参考单次返回多少数据，不是说要严格按照这个。size单位应该是字节。必须返回的是 utf8 编码的
 * */
export function createReadableStream(
  getData: (size: number) => Promise<string | null>
) {
  return new stream.Readable({
    async read(size) {
      while (true) {
        let data = null
        try {
          data = await getData(size)
        } catch (e) {
          console.error('[h-node-utils-error createReadable]:', e)
        }
        const goContinue = this.push(data, 'utf8')
        if (!goContinue || data === null) {
          break
        }
      }
    },
  })
}

/**
 * 创建一个 stream.Writable ，写入这个stream的内容会通过回调的方式返回
 * 跟fs.createWriteStream不一样，不会把写入stream的内容存到文件里，而是直接返回
 *
 * @param resolve 读取完成后会回调该函数，并把被写入的数据传入该函数第一个参数
 * @return {module:stream.internal.Writable} 返回的是一个可写入的 Stream，往里面写东西，结束后会回调成功函数
 * */
export function createWriteStream(
  resolve: (buffer: Buffer) => void
): stream.Writable {
  const chunks: (Buffer | string)[] = []
  const ws = new stream.Writable({
    write(chunk, encoding, callback) {
      chunks.push(chunk)
      callback()
    },
  })
  ws.on('finish', () => {
    let data
    if (Buffer.isBuffer(chunks[0])) {
      data = Buffer.concat(chunks as Buffer[])
    } else {
      data = Buffer.from(chunks.join(''))
    }
    resolve(data)
  })
  return ws
}

/**
 * 从一个可读stream里读数据出来
 * 一般用stream了，表示内容挺大，所以用这个方法时要小心，内存不要爆了
 * */
export function readFromReadableStream(readableStream: fs.ReadStream) {
  return new Promise(resolve => {
    readableStream.pipe(createWriteStream(resolve))
  })
}

/**
 * 压缩文件
 * @param {array} files 要打包的文件路径
 * @param {string} zipFile 生成的zip文件完整路径
 * */
export async function zip(files: string[], zipFile: string) {
  const archiver = require('archiver')

  return new Promise(resolve => {
    // create a file to stream archive data to.
    const output = fs.createWriteStream(zipFile)
    const archive = archiver('zip', {
      zlib: {level: 9}, // Sets the compression level.
    })

    // listen for all archive data to be written
    // 'close' event is fired only when a file descriptor is involved
    output.on('close', function () {
      // console.log('close？？？')
      // console.log(archive.pointer() + ' total bytes')
      // console.log('archiver has been finalized and the output file descriptor has closed.')
      resolve([200, ''])
    })

    // This event is fired when the data source is drained no matter what was the data source.
    // It is not part of this library but rather from the NodeJS Stream API.
    // @see: https://nodejs.org/api/stream.html#stream_event_end
    // output.on('end', function () {
    //   console.log('end？？？')
    //   console.log('Data has been drained')
    // })

    // output.on('finish', function () {
    //   console.log('完成？？？')
    //   resolve([ 200, '' ])
    // })

    // good practice to catch warnings (ie stat failures and other non-blocking errors)
    archive.on('warning', function (err: any) {
      console.log('【压缩文件】警告', err.stack)
    })

    // good practice to catch this error explicitly
    archive.on('error', function (err: any) {
      console.log('【压缩文件】错误', err.stack)
      resolve([0, err.msg, err])
    })

    // pipe archive data to the file
    archive.pipe(output)

    // append a file
    files.forEach(f => {
      const info = f.split(path.sep)
      archive.file(f, {name: info[info.length - 1]})
    })

    // finalize the archive (ie we are done appending files but streams have to finish yet)
    // 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand
    archive.finalize()
  })
}

// 删除目录及里面的文件， rmdir只能删除空文件夹
export async function deleteFolder(dir: string) {
  const files = await readdir(dir)
  if (files) {
    for (const f of files) {
      const curPath = path.join(dir, f)
      if (fs.statSync(curPath).isDirectory()) {
        // recurse
        await deleteFolder(curPath)
      } else {
        // delete file
        await unlink(curPath)
      }
    }
  }
  await rmdir(dir)
  return true
}
